﻿namespace MefContrib.Models.Provider
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel.Composition.Primitives;
    using System.Linq;
    using System.Reflection;
    using MefContrib.Models.Provider.Validation;
    using System.ComponentModel.Composition;

    /// <summary>
    /// 
    /// </summary>
    public abstract class ComposableMember
    {
        static ComposableMember()
        {
            _ToLazyTM = typeof (ComposableMember).GetMethod("ToLazyTM", BindingFlags.NonPublic | BindingFlags.Static);
            _ToLazyT = typeof(ComposableMember).GetMethod("ToLazyT", BindingFlags.NonPublic | BindingFlags.Static);
        }
        

        private static MethodInfo _ToLazyTM;
        private static MethodInfo _ToLazyT;


        /// <summary>
        /// Initializes a new instance of the <see cref="ComposableMember"/> class, for the
        /// specified <see cref="MemberInfo"/> instance.
        /// </summary>
        /// <param name="member">The member to make exportable.</param>
        protected ComposableMember(MemberInfo member)
        {
            this.Member = member;
            //this.ReturnType = returnType;
        }

        /// <summary>
        /// Gets the type of the object which the exportable member belongs to.
        /// </summary>
        /// <value>A <see cref="Type"/> object.</value>
        public virtual Type DeclaringType
        {
            get { return this.Member.DeclaringType; }
        }

        /// <summary>
        /// Gets the generic element type of the of the <see cref="ComposableMember"/>.
        /// </summary>
        /// <value>A <see cref="Type"/> object</value>
        private Type _elementType;

        public Type ElementType
        {
            get
            {
                if (_elementType == null)
                    _elementType = this.GetElementType();
                return _elementType;
            }
        }

        /// <summary>
        /// Gets a value indicating whether or not type of the <see cref="ComposableMember"/> is of a lazy type or not.
        /// </summary>
        /// <value><see langword="true" /> if it is of a known lazy type; otherwise <see langword="false" />.</value>
        public abstract bool IsLazyType { get; }

        /// <summary>
        /// Gets a value indicating whether or not a value can be retrieved from the member.
        /// </summary>
        /// <value><see langword="true" /> if a value can be retrieved; otherwise <see langword="false" />.</value>
        public abstract bool IsReadable { get; }

        /// <summary>
        /// Gets a value indicating whether or not an instance is needed for the value to be retrieved or assigned.
        /// </summary>
        /// <value><see langword="true" /> if an instance is needed; otherwise <see langword="false" />.</value>
        public abstract bool IsInstanceNeeded { get; }

        /// <summary>
        /// Gets a value indicating whether or not a value can be assigned to the member.
        /// </summary>
        /// <value><see langword="true" /> if a value can be assigned; otherwise <see langword="false" />.</value>
        public abstract bool IsWritable { get; }

        /// <summary>
        /// Gets or sets the member that is wrapped by the <see cref="ComposableMember"/>.
        /// </summary>
        /// <value>A <see cref="MemberInfo"/> object.</value>
        protected MemberInfo Member { get; set; }

        /// <summary>
        /// Gets the <see cref="Type"/> of the return value of the <see cref="ComposableMember"/>.
        /// </summary>
        /// <value>A <see cref="Type"/> object.</value>
        public abstract Type ReturnType { get; }

        /// <summary>
        /// Gets or sets the function used to get the value of the member.
        /// </summary>
        /// <value>A <see cref="Func{T1,TResult}"/> object.</value>
        protected Func<object, object> ValueGetter { get; set; }

        /// <summary>
        /// Gets or sets the function used to assign a value to the member.
        /// </summary>
        /// <value>A <see cref="Action{T1,T2}"/> object.</value>
        protected Action<object, object> ValueSetter { get; set; }

        /// <summary>
        /// Creates a new <see cref="ComposableMember"/> for the specified <paramref name="member"/>.
        /// </summary>
        /// <param name="member">The <see cref="MemberInfo"/> to create the <see cref="ComposableMember"/> for.</param>
        /// <returns>A <see cref="ComposableMember"/> object.</returns>
        /// <exception cref="ArgumentNullException">The specified value for the <see cref="member"/> parameter cannot be null.</exception>
        /// <exception cref="InvalidOperationException">The specified member was of an unsupported <see cref="MemberTypes"/>.</exception>
        public static ComposableMember Create(MemberInfo member)
        {
            member
                .Require("member")
                .NotNull();

            switch (member.MemberType)
            {
                case MemberTypes.Field:
                    return new ComposableField(member);

                case MemberTypes.Method:
                    return new ComposableMethod(member);

                case MemberTypes.Property:
                    return new ComposableProperty(member);

                case MemberTypes.NestedType:
                case MemberTypes.TypeInfo:
                    return new ComposableType(member);
            }

            throw new InvalidOperationException("Unsupported member type.");
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="exports"></param>
        /// <returns></returns>
        public object GetImportValueFromExports(IEnumerable<Export> exports)
        {
            return this.ReturnType.IsEnumerable() ?
                this.GetImportValueCollection(exports) : this.GetImportValueSingle(exports);
        }

        /// <summary>
        /// Gets the value of the member.
        /// </summary>
        /// <param name="instance">The instance to get the value from.</param>
        /// <returns>The value of the member.</returns>
        /// <exception cref="InvalidOperationException">No function for retrieving the value has been set.</exception>
        public virtual object GetValue(object instance)
        {
            if (this.ValueGetter == null)
            {
                throw new InvalidOperationException("No function for retrieving the value has been set.");
            }

            return this.ValueGetter.Invoke(instance);
        }

        /// <summary>
        /// Sets the value of the member to the specified value.
        /// </summary>
        /// <param name="instance">The instance to assign the specified value to.</param>
        /// <param name="value">The value to assign to the property.</param>
        /// <exception cref="InvalidOperationException">Either the member is not writeable or oo function for assigning the value has been set.</exception>
        public virtual void SetValue(object instance, object value)
        {
            if (!this.IsWritable || this.ValueSetter == null)
            {
                throw new InvalidOperationException("Either the member is not writeable or oo function for assigning the value has been set.");
            }

            this.ValueSetter.Invoke(instance, value);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        private Type GetElementType()
        {
            if (this.ReturnType.IsEnumerable())
            {
                Type[] collectionInterfaces =
                    new[] { typeof(ICollection<>), typeof(IEnumerable<>), typeof(IList<>) };

                if (ReturnType.IsGenericType)
                {
                    var genericTypeDefinition = ReturnType.GetGenericTypeDefinition();
                    if (collectionInterfaces.Contains(genericTypeDefinition))
                        return ReturnType.GetGenericArguments()[0];
                }
                
                foreach (Type targetInterface in ReturnType.GetInterfaces().Where(i=>i.IsGenericType))
                {
                    var genericTypeDefinition = targetInterface.GetGenericTypeDefinition();
                    if (collectionInterfaces.Contains(genericTypeDefinition))
                    {
                        return targetInterface.GetGenericArguments()[0];       
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="exports"></param>
        /// <returns></returns>
        private object GetImportValueCollection(IEnumerable<Export> exports)
        {
            
            if (exports.Count() == 0)
            {
                return null;
            }

            bool isLazy = this.ElementType.IsLazyType();

            if (!isLazy)
            {
                Type[] genericTypeInformation =
                    this.ReturnType.GetGenericArguments();

                Type collectionType =
                    typeof(List<>).MakeGenericType(genericTypeInformation[0]);
                IList collection = (IList)Activator.CreateInstance(collectionType);

                foreach (Export e in exports)
                {
                    collection.Add(e.Value);
                }

                return collection;
            }

            IList exportCollection =
                (IList)Activator.CreateInstance(this.ToLazyType(), null);

            MethodInfo lazyFactory;
            if (isLazy)
            {
                var genericArgs = ElementType.GetGenericArguments();
                Type contractType = genericArgs[0];
                if (genericArgs.Length == 2)
                {
                    Type metadataViewType = genericArgs.Length == 2 ? genericArgs[1] : typeof(IDictionary<string, object>);
                    lazyFactory = _ToLazyTM.MakeGenericMethod(contractType, metadataViewType);
                }
                else
                    lazyFactory = _ToLazyT.MakeGenericMethod(contractType);
            }   
            else
            {
                lazyFactory = _ToLazyT.MakeGenericMethod(ElementType);
            }

            foreach (Export item in exports)
            {
                var typed = lazyFactory.Invoke(null, new[] {item});
                exportCollection.Add(typed);
            }

            return exportCollection;
        }

        private static Lazy<T, TMetadata> ToLazyTM<T, TMetadata>(Export export)
        {
            Func<T> valueFactory = () => (T)export.Value;
            TMetadata metadata = AttributedModelServices.GetMetadataView<TMetadata>(export.Metadata);
            return new Lazy<T, TMetadata>(valueFactory, metadata);
        }

        private static Lazy<T> ToLazyT<T>(Export export)
        {
            Func<T> valueFactory = () => (T)export.Value;
            return new Lazy<T>(valueFactory);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="exports"></param>
        /// <returns></returns>
        private object GetImportValueSingle(IEnumerable<Export> exports)
        {
            Export single =
                exports.FirstOrDefault();

            if (single == null)
            {
                return null;
            }

            MethodInfo lazyFactory;

            if (!this.ReturnType.IsLazyType())
            {
                return single.Value;
            }

            if (!this.ReturnType.IsGenericType)
            {
                return single;
            }

            var genericArgs = ReturnType.GetGenericArguments();
            Type contractType = genericArgs[0];
            if (genericArgs.Length == 2)
            {
                Type metadataViewType = genericArgs.Length == 2 ? genericArgs[1] : typeof(IDictionary<string, object>);
                lazyFactory = _ToLazyTM.MakeGenericMethod(contractType, metadataViewType);
            }
            else
                lazyFactory = _ToLazyT.MakeGenericMethod(contractType);

            return lazyFactory.Invoke(null, new[] { single });

        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public Type ToLazyType()
        {
            return this.ElementType != null ?
                this.ElementType.ToLazyCollectionType() : this.ReturnType.ToLazyType();
        }
    }    
}